Цель исследования - провести оценку результатов A/B-теста изменений, связанных с внедрением улучшенной рекомендательной системы интернет-магазина.
Техническое задание
# импорт библиотек
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import scipy.stats as stats
import os
import math as mth
from matplotlib. ticker import PercentFormatter
# импортируем функции для работы со временем
from datetime import datetime, timedelta
# импортируем библиотеку для работы со воронками
from plotly import graph_objects as go
# чтение файлов с данными и сохранение в талицы
ab_project_marketing_events = pd.read_csv('/datasets/ab_project_marketing_events.csv', parse_dates=['start_dt','finish_dt'])
final_ab_new_users = pd.read_csv('/datasets/final_ab_new_users.csv', parse_dates=['first_date'])
final_ab_events = pd.read_csv('/datasets/final_ab_events.csv', parse_dates=['event_dt'])
final_ab_participants = pd.read_csv('/datasets/final_ab_participants.csv')
# получаем общую информации о данных в таблице ab_project_marketing_events
print(ab_project_marketing_events.info())
display(ab_project_marketing_events.head())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 14 entries, 0 to 13 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 name 14 non-null object 1 regions 14 non-null object 2 start_dt 14 non-null datetime64[ns] 3 finish_dt 14 non-null datetime64[ns] dtypes: datetime64[ns](2), object(2) memory usage: 576.0+ bytes None
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
| 1 | St. Valentine's Day Giveaway | EU, CIS, APAC, N.America | 2020-02-14 | 2020-02-16 |
| 2 | St. Patric's Day Promo | EU, N.America | 2020-03-17 | 2020-03-19 |
| 3 | Easter Promo | EU, CIS, APAC, N.America | 2020-04-12 | 2020-04-19 |
| 4 | 4th of July Promo | N.America | 2020-07-04 | 2020-07-11 |
В таблице 'ab_project_marketing_events' 4 столбца, 14 строк. В каждой строке информация о маркетинговой компании на 2020 год. Типы данных datetime64(2), object(2).
# получаем общую информации о данных в таблице ab_project_marketing_events
print(final_ab_new_users.info())
display(final_ab_new_users.head())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 61733 entries, 0 to 61732 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 61733 non-null object 1 first_date 61733 non-null datetime64[ns] 2 region 61733 non-null object 3 device 61733 non-null object dtypes: datetime64[ns](1), object(3) memory usage: 1.9+ MB None
| user_id | first_date | region | device | |
|---|---|---|---|---|
| 0 | D72A72121175D8BE | 2020-12-07 | EU | PC |
| 1 | F1C668619DFE6E65 | 2020-12-07 | N.America | Android |
| 2 | 2E1BF1D4C37EA01F | 2020-12-07 | EU | PC |
| 3 | 50734A22C0C63768 | 2020-12-07 | EU | iPhone |
| 4 | E1BDDCE0DAFA2679 | 2020-12-07 | N.America | iPhone |
# выводим минимальное и максимальное значения в столбце first_date в таблице final_ab_new_users
print('Минимальное значения в столбце first_date в таблице final_ab_new_users - ', final_ab_new_users['first_date'].min())
print('Максимальное значения в столбце first_date в таблице final_ab_new_users - ', final_ab_new_users['first_date'].max())
Минимальное значения в столбце first_date в таблице final_ab_new_users - 2020-12-07 00:00:00 Максимальное значения в столбце first_date в таблице final_ab_new_users - 2020-12-23 00:00:00
В таблице 'final_ab_new_users' 4 столбца, 61733 строки. В каждой строке информация о пользователях, зарегистрировавшихся в интернет-магазине в период с 7 по 23 декабря 2020 года. Типы данных datetime64(1), object(3).
# получаем общую информации о данных в таблице ab_project_marketing_events
print(final_ab_events.info())
display(final_ab_events.head())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 440317 entries, 0 to 440316 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 440317 non-null object 1 event_dt 440317 non-null datetime64[ns] 2 event_name 440317 non-null object 3 details 62740 non-null float64 dtypes: datetime64[ns](1), float64(1), object(2) memory usage: 13.4+ MB None
| user_id | event_dt | event_name | details | |
|---|---|---|---|---|
| 0 | E1BDDCE0DAFA2679 | 2020-12-07 20:22:03 | purchase | 99.99 |
| 1 | 7B6452F081F49504 | 2020-12-07 09:22:53 | purchase | 9.99 |
| 2 | 9CD9F34546DF254C | 2020-12-07 12:59:29 | purchase | 4.99 |
| 3 | 96F27A054B191457 | 2020-12-07 04:02:40 | purchase | 4.99 |
| 4 | 1FD7660FDF94CA1F | 2020-12-07 10:15:09 | purchase | 4.99 |
# выводим минимальное и максимальное значения в столбце event_dt в таблице final_ab_events
print('Минимальное значения в столбце event_dt в таблице final_ab_events - ',final_ab_events['event_dt'].min())
print('Максимальное значения в столбце event_dt в таблице final_ab_events - ',final_ab_events['event_dt'].max())
Минимальное значения в столбце event_dt в таблице final_ab_events - 2020-12-07 00:00:33 Максимальное значения в столбце event_dt в таблице final_ab_events - 2020-12-30 23:36:33
В таблице 'final_ab_events' 4 столбца, 440 317 строк. В каждой строке информация о событиях новых пользователей в период с 7 декабря 2020 по 30 декабря 2020 года. Типы данных datetime64(1), float64(1), object(2).
# получаем общую информации о данных в таблице ab_project_marketing_events
print(final_ab_participants.info())
display(final_ab_participants.head())
<class 'pandas.core.frame.DataFrame'> RangeIndex: 18268 entries, 0 to 18267 Data columns (total 3 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 user_id 18268 non-null object 1 group 18268 non-null object 2 ab_test 18268 non-null object dtypes: object(3) memory usage: 428.3+ KB None
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
В таблице 'final_ab_participants' 3 столбца, 18 268 строк. В каждой строке информация об участнике тестов. Типы данных object(3).
Вывод после обзора данных
Предварительно можно утверждать, что данных достаточно для анализа. Но в таблице final_ab_events есть информация о событиях только с 7 по 30 декабря 2020 года. В описании к данным сказано, что таблица final_ab_events содержит все события новых пользователей в период с 7 декабря 2020 по 4 января 2021 года. Либо новые пользователи не совершали событий после 30 декабря, либо это ошибка в формировании данных и у нас не полные данные. Необходимо обратиться к коллегам, которые отвечают за выгрузку данных и уточнить этот вопрос у них.
Типы данных соответствуют значениям, преобразвание типов не требуется.
# проверяем дубликаты и пропуски в таблице ab_project_marketing_events
print('Количество дубликатов в таблице ab_project_marketing_events - ',ab_project_marketing_events.duplicated().sum())
print('Количество пропусков в таблице ab_project_marketing_events')
print(ab_project_marketing_events.isna().sum())
Количество дубликатов в таблице ab_project_marketing_events - 0 Количество пропусков в таблице ab_project_marketing_events name 0 regions 0 start_dt 0 finish_dt 0 dtype: int64
В таблице ab_project_marketing_events дубликатов и пропусков не обнаружено.
# проверяем дубликаты и пропуски в таблице final_ab_new_users
print('Количество дубликатов в таблице final_ab_new_users - ',final_ab_new_users.duplicated().sum())
print('Количество пропусков в таблице final_ab_new_users')
print(final_ab_new_users.isna().sum())
Количество дубликатов в таблице final_ab_new_users - 0 Количество пропусков в таблице final_ab_new_users user_id 0 first_date 0 region 0 device 0 dtype: int64
В таблице final_ab_new_users дубликатов и пропусков не обнаружено.
# проверяем дубликаты и пропуски в таблице final_ab_events
print('Количество дубликатов в таблице final_ab_events - ',final_ab_events.duplicated().sum())
print('Количество пропусков в таблице final_ab_events')
print(final_ab_events.isna().sum())
Количество дубликатов в таблице final_ab_events - 0 Количество пропусков в таблице final_ab_events user_id 0 event_dt 0 event_name 0 details 377577 dtype: int64
В таблице final_ab_events дубликатов не обнаружено. Пропуски есть только в столбце details, но это поле не обязательно к заполнению и содержит дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.
# проверяем дубликаты и пропуски в таблице final_ab_participants
print('Количество дубликатов в таблице final_ab_participants - ',final_ab_participants.duplicated().sum())
print('Количество пропусков в таблице final_ab_participants')
print(final_ab_participants.isna().sum())
Количество дубликатов в таблице final_ab_participants - 0 Количество пропусков в таблице final_ab_participants user_id 0 group 0 ab_test 0 dtype: int64
В таблице final_ab_participants дубликатов и пропусков не обнаружено.
Выводы после предобработки
Дубликатов в таблицах не обнаружено. Пропуски есть только в таблице final_ab_events в столбце details, но это поле не обязательно к заполнению и содержит дополнительные данные о событии. Например, для покупок, purchase, в этом поле хранится стоимость покупки в долларах.
# выводим информацию обо всех АВ тестах, которые проводились
final_ab_participants['ab_test'].value_counts()
interface_eu_test 11567 recommender_system_test 6701 Name: ab_test, dtype: int64
Всего в данных есть информация о двух АВ тестах - 'recommender_system_test' и 'interface_eu_test'. Мы проверяем 'recommender_system_test'. В таблице есть информация о 6701 пользователе, которые принимали участие в тесте.
# собираем пользователей, которые попали в нужный нам тест
users = final_ab_participants.query('ab_test == "recommender_system_test"')
users.head()
| user_id | group | ab_test | |
|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test |
| 1 | A7A3664BD6242119 | A | recommender_system_test |
| 2 | DABC14FDDFADD29E | A | recommender_system_test |
| 3 | 04988C5DF189632E | A | recommender_system_test |
| 4 | 482F14783456D21B | B | recommender_system_test |
# добавляем информацию о пользователях из таблицы final_ab_new_users
users = users.merge(final_ab_new_users, on='user_id', how='left')
users.head()
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC |
| 1 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone |
| 2 | DABC14FDDFADD29E | A | recommender_system_test | 2020-12-08 | EU | Mac |
| 3 | 04988C5DF189632E | A | recommender_system_test | 2020-12-14 | EU | iPhone |
| 4 | 482F14783456D21B | B | recommender_system_test | 2020-12-14 | EU | PC |
# выводим регион участников из нашего теста
print(users['region'].value_counts())
users = users.query('region == "EU"')
EU 6351 N.America 223 APAC 72 CIS 55 Name: region, dtype: int64
В тест попали пользователи из разных регионов, по ТЗ нас интересуют пользователи из EU, поэтому оставили только таких пользователей.
# выводим количество новых пользователей из региона EU
EU_new_users = final_ab_new_users.query('region == "EU" & "2020-12-07" <= first_date <= "2020-12-21"')['user_id'].count()
print("Новые пользователи из региона EU - " ,EU_new_users)
# выводим долю участников теста от новых пользователей из региона EU
print("Доля участников теста от новых пользователей из региона EU - " ,"{0:.2%}".format(users['user_id'].count() / EU_new_users))
Новые пользователи из региона EU - 42340 Доля участников теста от новых пользователей из региона EU - 15.00%
По ТЗ учасники нашего теста это 15% от новых пользователей из европы, так и есть.
# соберем в таблицу пользователей, которые участвуют в двух тестах одновременно
intersection_test = final_ab_participants.groupby('user_id', as_index=False).agg({'ab_test':'count'}).query('ab_test > 1')
# отсеим тех, кто попал в контрольную группу в interface_eu_test
iet_b = final_ab_participants.query("ab_test == 'interface_eu_test' & group == 'B'")
intersection_test = intersection_test.query("user_id in @iet_b['user_id']")
print('Пользователи, участвующие в нашем тесте и группе В конкурирующего теста - ',intersection_test['user_id'].count())
Пользователи, участвующие в нашем тесте и группе В конкурирующего теста - 783
В наших данных есть 783 пользователя, которые пересекаются с конкурирующим тестом и видели в нем изменения. Такие пользователи могут вести себя не так как остальные участники теста, так как на них влияет изменения в конкурирующем тесте, это в свою очередь может повлиять на результат анализа. Исключим таких пользователей из выборок.
# собираем пользователей, которые попали в нужный нам тест, за исключением тех, кто учавствует в двух тестах одновременно
users = users.query('user_id not in @intersection_test["user_id"]')
users.head()
| user_id | group | ab_test | first_date | region | device | |
|---|---|---|---|---|---|---|
| 0 | D1ABA3E2887B6A73 | A | recommender_system_test | 2020-12-07 | EU | PC |
| 1 | A7A3664BD6242119 | A | recommender_system_test | 2020-12-20 | EU | iPhone |
| 2 | DABC14FDDFADD29E | A | recommender_system_test | 2020-12-08 | EU | Mac |
| 3 | 04988C5DF189632E | A | recommender_system_test | 2020-12-14 | EU | iPhone |
| 4 | 482F14783456D21B | B | recommender_system_test | 2020-12-14 | EU | PC |
# соберем в таблицу пользователей, которые участвуют в двух группах одновременно
intersection_group = users.groupby('user_id', as_index=False).agg({'group':'count'}).query('group > 1')
print('Пользователи, которые участвуют в двух группах одновременно - ', intersection_group['user_id'].count())
Пользователи, которые участвуют в двух группах одновременно - 0
Нет пользователей, участвующих в двух группах теста одновременно.
# выводим количество участников теста после фильтрации
print('Количество участников теста, после фильтрации по ТЗ - ', users['user_id'].count())
Количество участников теста, после фильтрации по ТЗ - 5568
После всех проверок на соответствие ТЗ у нас осталось 5568 пользователей, ожидалось 6000 по ТЗ.
# выводим распределение участников теста, по группам
print('Распределение участников теста, по группам')
print(users['group'].value_counts())
Распределение участников теста, по группам A 3195 B 2373 Name: group, dtype: int64
print('Доля пользователей в группе А - ','{0:.2%}'.format(users.query('group == "A"')['user_id'].count() / users['user_id'].count()))
print('Доля пользователей в группе B - ','{0:.2%}'.format(users.query('group == "B"')['user_id'].count() / users['user_id'].count()))
Доля пользователей в группе А - 57.38% Доля пользователей в группе B - 42.62%
Распределение на группы не совсем равномерное. В группе А больше пользователей чем в группе В, доля пользователей в группе А 57,4%. Проведем анализ эксперемента с такими данными, но в будущем лучше более равномерно распределять пользователей по группам.
# выводим минимальное и максимальное значения в столбце first_date в таблице users
print('Минимальное значения в столбце first_date в таблице users - ', users['first_date'].min())
print('Максимальное значения в столбце first_date в таблице users - ', users['first_date'].max())
Минимальное значения в столбце first_date в таблице users - 2020-12-07 00:00:00 Максимальное значения в столбце first_date в таблице users - 2020-12-21 00:00:00
Минимальное значение first_date соответствует началу теста. Максимальное значения соответствует дате остановки набора новых пользователей.
# создаем таблицу с событиями пользователей, которые участвуют в нашем тесте
events = final_ab_events.query('user_id in @users["user_id"]')
# выводим количество пользователей, к которых было хотя бы одно событие
print('Количество пользователей, у которых было хотя бы одно событие - ' ,events['user_id'].nunique())
Количество пользователей, у которых было хотя бы одно событие - 3050
Из 5568 учасников всего у 3050 пользователей было хотя бы одно событие.
# удаляем события старше 14 дней с регистрации пользователя
events = events.merge(users, on = 'user_id')
events['date'] = events['event_dt'].dt.date
events['date'] = pd.to_datetime(events['event_dt'])
events['days'] = (events['date'] - events['first_date']).dt.days
events = events.loc[events['days'] <= 14]
events['date'] = events['date'].dt.date
events = events.drop(columns = 'days')
# выводим минимальное и максимальное значения в столбце date в таблице final_ab_events
print('Минимальное значения в столбце event_dt в таблице events - ',events['event_dt'].min())
print('Максимальное значения в столбце event_dt в таблице events - ',events['event_dt'].max())
Минимальное значения в столбце event_dt в таблице events - 2020-12-07 00:05:57 Максимальное значения в столбце event_dt в таблице events - 2020-12-29 23:38:29
Минимальное значение event_dt соответствует началу теста. Максимальное значение меньше даты остановки теста. Можно предположить, что после 29 декабря новые пользователи не совершали новых событий.
# выводим маркетинговые компании, которые проводились в течении нашего теста
ab_project_marketing_events.query("@events['event_dt'].min() < start_dt < @events['event_dt'].max() | @events['event_dt'].min() < finish_dt < @events['event_dt'].max()")
| name | regions | start_dt | finish_dt | |
|---|---|---|---|---|
| 0 | Christmas&New Year Promo | EU, N.America | 2020-12-25 | 2021-01-03 |
Во время теста проходила маркетинговая компания Christmas&New Year Promo направленная на пользователей из нашего эксперимента. Компания началась 25 декабря 2020 года и шла почти до конца эксперемента, до 3 января 2021 года.
# выводим гистограмму распределения событий по дате и времени
plt.figure(figsize=(20,5))
events['event_dt'].hist(bins=24*24)
plt.title("Гистограмма распределения событий по дате и времени")
plt.xlabel("Дата")
plt.ylabel("Частота")
plt.show()
На гистограмме видно, что рост числа событй начинается после 13 декабря и заканчивается к 25 декабря, можно сделать вывод, что маркетинговая компания не сильно влияет на количество событий. Проведем анализ эксперемента не смотря на маркетинговую компанию, которая проходила в период проведения эксперемента, но в будущем таких совпадений лучше избегать.
Выводы после проверки корректности проведения теста
Первоначально количество участников теста составляло 6701, после удаления участников не из Европы у нас остался 6351 участник, что составляет ровно 15% от новых пользователей из Европы, зарегистрировашихся в период с 7 по 21 декабря 2020г.
Однако, после удаление части пользователей, которые учавствовали в конкурирующем тесте и видели в нем изменения, у нас осталось 5568 участников, в то время как ожидалось 6000 участников. В будущем рекомендуем не допускать пересечений тестов.
Распределение на группы не совсем равномерное. В группе А больше пользователей чем в группе В, доля пользователей в группе А 57,4%. В будущем нужно более равномерно распределять пользователей по группам.
Дата запуска и дата регистрации первых пользователей совпадают - 2020-12-07
Дата остановки набора новых пользователей и дата регистрации последних пользователей совпадают - 2020-12-21
Из 5568 учасников всего у 3050 пользователей было хотя бы одно событие.
Дата запуска и дата первых событий совпадают - 2020-12-07
Дата остановки теста и дата последних событий не совпадают:
Дата последних событий на 6 дней меньше даты остановки теста. Либо новые пользователи не совершали событий после 29 декабря, либо это ошибка в формировании данных и у нас не полные данные. Необходимо обратиться к коллегам, которые отвечают за выгрузку данных и уточнить этот вопрос у них.
Во время теста проходила маркетинговая компания Christmas&New Year Promo направленная на пользователей из нашего эксперимента. Компания началась 25 декабря 2020 года и шла до 3 января 2021 года. Маркетинговая компания не сильно влияет на количество событий. В будущем таких совпадений лучше избегать.
# создаем таблицу с событиями пользователей, которые участвуют в группе А
a_events = events.query('group == "A"')
# создаем таблицу с событиями пользователей, которые участвуют в группе В
b_events = events.query('group == "B"')
# выводим информацию о медианном
print('Медианное количество событий на пользователя в группе А - ' ,'{0:.2}'.format(a_events.groupby('user_id').agg({'event_dt':'count'})['event_dt'].median()))
print('Медианное количество событий на пользователя в группе В - ' ,'{0:.2}'.format(b_events.groupby('user_id').agg({'event_dt':'count'})['event_dt'].median()))
Медианное количество событий на пользователя в группе А - 6.0 Медианное количество событий на пользователя в группе В - 4.0
Медианное количество событий на пользователя в группе А больше, чем в группе В.
# устанавливаем размеры графика
plt.figure(figsize=(12,5))
# Строим график распределения количества событий по дням группы А
plt.plot(a_events.groupby('date', as_index=False).agg({'event_dt':'count'})['date'], a_events.groupby('date', as_index=False).agg({'event_dt':'count'})['event_dt'], label='A')
# Строим график распределения количества событий по дням группы B
plt.plot(b_events.groupby('date', as_index=False).agg({'event_dt':'count'})['date'], b_events.groupby('date', as_index=False).agg({'event_dt':'count'})['event_dt'], label='B')
plt.legend()
plt.title("График распределения количества событий по дням")
plt.xlabel("Дата")
plt.ylabel("Количество событий")
plt.legend()
plt.show()
Количество событий в группе А начинает резко расти с 14 декабря 2020 г. и достигает пика 21 декабря 2020 г., далее плавно снижается и обрывается 29 декабря 2020 г.
Количество событий группы В сначала снижается до 13 декабря, потом начинает расти до пика 16 декабря 2020 г. , потом небольшое падение и снова пик 21 декабря 2020г. далее снижение до 29 декабря 2020 г.,
Данных после 29 декабря 2020 г. нет.
funnel = events.pivot_table(index='event_name', columns='group',values='user_id',aggfunc='nunique').reset_index()
funnel['CR A'] = round ((funnel['A'] / events.query('group == "A" & event_name == "login"')['user_id'].nunique()*100),2)
funnel['CR B'] = round ((funnel['B'] / events.query('group == "B" & event_name == "login"')['user_id'].nunique()*100),2)
new_index = {1: 2, 2: 1}
funnel = funnel.rename(new_index).sort_index()
funnel
| group | event_name | A | B | CR A | CR B |
|---|---|---|---|---|---|
| 0 | login | 2279 | 770 | 100.00 | 100.00 |
| 1 | product_page | 1476 | 429 | 64.77 | 55.71 |
| 2 | product_cart | 686 | 214 | 30.10 | 27.79 |
| 3 | purchase | 734 | 219 | 32.21 | 28.44 |
В группе А конверсия в просмотр карточки товара составила 64.8%, в просмотр корзины 30.1%, в покупку 32.2%.
В группе В конверсия в просмотр карточки товара составила 55.6%, в просмотр корзины 27.8%, в покупку 28.4%.
Конверсия покупку больше, чем в просмотр корзины, возможно покупку можно совершить без просмотра корзины.
Конверсия в группе В ниже, чем в группе А.
fig = go.Figure()
fig.add_trace(go.Funnel(
name = 'Group A',
y = funnel['event_name'],
x = funnel['A'],
textinfo = "value+percent initial",
))
fig.add_trace(go.Funnel(
name = 'Group B',
y = funnel['event_name'],
x = funnel['B'],
textinfo = "value+percent initial",
))
fig.show()
На графике хорошо видно, насколько неравномерное разделение пользователей по группам, в группе А больше пользователей, чем в группе В.
Видно, что конверсия в группе В меньше, чем в группе А на 2-9%.
Вывод после исследовательского анализа данных
Медианное количество событий на пользователя в группе А больше, чем в группе В.
Количество событий в группе А начинает резко расти с 14 декабря 2020 г. и достигает пика 21 декабря 2020 г., далее плавно снижается и обрывается 29 декабря 2020 г.
Количество событий группы В сначала снижается до 13 декабря, потом начинает расти до пика 16 декабря 2020 г. , потом небольшое падение и снова пик 21 декабря 2020г. далее снижение до 29 декабря 2020 г.,
Данных после 29 декабря 2020 г. нет.
В группе А конверсия в просмотр карточки товара составила 64.8%, в просмотр корзины 30.1%, в покупку 32.2%.
В группе В конверсия в просмотр карточки товара составила 55.6%, в просмотр корзины 27.8%, в покупку 28.4%.
Конверсия по всем метрикам в группе В ниже, чем в группе А.
Особенности данных, которые нужно учесть, прежде чем приступать к A/B-тестированию
Исследуем результаты A/B-эксперимента о изменениях, связанных с внедрением улучшенной рекомендательной системы. Пользователей разбили на 2 группы: А (контрольная), B (новая платёжная воронка).
Сравним доли пользователей, совершивших одно и тоже событие, в разных группах с помощью Z-критерия двух пропорций.
Критический уровень статистической значимости применим ɑ = 0,05.
При этом мы собираемся проверить 3 статистических гипотезы:
Чтобы снизить вероятность ложнопозитивного результата при множественном тестировании гипотез применим поправку Бонферрони, т.е. разделим ɑ на количество гипотез.
ɑ = 0.05 / 3 = 0.017
# напишем функцию для проверки статистической значимости разницы между группами
def z_test (successes, sample):
alpha = .017 # критический уровень статистической значимости с поправкой Бонферрони
# пропорция успехов в первой группе:
p1 = successes[0]/sample[0]
# пропорция успехов во второй группе:
p2 = successes[1]/sample[1]
# пропорция успехов в комбинированном датасете:
p_combined = (successes[0] + successes[1]) / (sample[0] + sample[1])
# разница пропорций в датасетах
difference = p1 - p2
# считаем статистику в ст.отклонениях стандартного нормального распределения
z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1/sample[0] + 1/sample[1]))
# задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
distr = stats.norm(0, 1)
p_value = (1 - distr.cdf(abs(z_value))) * 2
print('p-значение: {0:.5f}'.format(p_value))
if p_value < alpha:
print('Отвергаем нулевую гипотезу: между долями есть значимая разница')
else:
print(
'Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными'
)
Проверим, находят ли статистические критерии разницу между выборками А (контрольная) и B (новая платёжная воронка).
Сформулируем гипотезы.
Нулевая: различий в долях пользователей, совершивших одно и тоже событие между группами А и В нет.
Альтернативная: различия в долях пользователей, совершивших одно и тоже событие между группами А и В есть.
for e in ['product_page','product_cart','purchase']:
successes = [a_events.query('event_name == @e')['user_id'].nunique(),
b_events.query('event_name == @e')['user_id'].nunique()]
sample = [a_events['user_id'].nunique(), b_events['user_id'].nunique()]
print('Провека для события ',e,' в группах A и B')
z_test(successes, sample)
print('')
Провека для события product_page в группах A и B p-значение: 0.00001 Отвергаем нулевую гипотезу: между долями есть значимая разница Провека для события product_cart в группах A и B p-значение: 0.21721 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными Провека для события purchase в группах A и B p-значение: 0.04894 Не получилось отвергнуть нулевую гипотезу, нет оснований считать доли разными
Значимая разница между долями есть в только для события product_page. Тоесть изменение конверсии в просмотр карточек товаров между группами статистически значимо.
Помотрим насколько изменилась конверсия в просмотр карточек товаров между группами.
# выводим конверсию в просмотр карточки товара в группе А и В и разницу между ними
print('Конверсия в просмотр карточек товаров в группе А - ', '{0:.2%}'.format(a_events.query('event_name == "product_page"')['user_id'].nunique() / a_events['user_id'].nunique()))
print('Конверсия в просмотр карточек товаров в группе B - ', '{0:.2%}'.format(b_events.query('event_name == "product_page"')['user_id'].nunique() / b_events['user_id'].nunique()))
print('Разница между группами А и В - ', '{0:.2%}'.format((a_events.query('event_name == "product_page"')['user_id'].nunique() / a_events['user_id'].nunique())-(b_events.query('event_name == "product_page"')['user_id'].nunique() / b_events['user_id'].nunique())))
Конверсия в просмотр карточек товаров в группе А - 64.77% Конверсия в просмотр карточек товаров в группе B - 55.64% Разница между группами А и В - 9.12%
Конверсия в просмотр карточки товаров в группе В ниже по сравнению с группой А на 9%.
Вывод после анализа результатов эксперимента
Мы исследовали результаты A/B-эксперимента о изменениях, связанных с внедрением улучшенной рекомендательной системы.
Пользователей разбили на 2 группы: А (контрольная), B (новая платёжная воронка).
Проверили 3 статистических гипотезы:
Значимая разница между долями есть в только для события product_page. Тоесть изменение конверсии в просмотр карточек товаров между группами статистически значимо.
Конверсия в просмотр карточки товаров в группе В ниже по сравнению с группой А на 9%.
Из чего можно сделать вывод, что новая платёжная воронка не улучшает метрики, а конверсию в просмотр карточек товаров даже делает хуже.
Мы проанализировали результаты A/B-эксперимента о изменениях, связанных с внедрением улучшенной рекомендательной системы. Проверили корректность проведения теста, убрали данные не соответствующие ТЗ. Изучили распределение количества событий на пользователя в выборках и распределение числа событий по дням, изучили воронки конверсий в каждой группе. Сравнили доли пользователей, совершивших одно и тоже событие, в разных группах с помощью Z-критерия двух пропорций.
Таким образом, новая платежная воронка не улучшает метрики, а даже ухудшает конверсию в просмотр карточек товаров.
Общий вывод - результаты эксперимента неоднозначные. Есть вопросы к качеству данных и дизайну эксперимента. Рекомендуем провести повторный эксперимент с учетом выявленных недостатков.